(*
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-2.1.html
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *)

namespace TfsIndexerTasks

open System
open System.IO
open System.Diagnostics
open System.Text.RegularExpressions
open TfsEngine

open Microsoft.Build.Framework
open Microsoft.Build.Utilities

type TfsIndex() = 
    inherit Task()

    let mutable tfsEngine: ITfsEngine option = None

    let relativePath file =
        let myFile = Reflection.Assembly.GetExecutingAssembly().Location
        let myPath = FileInfo(myFile).DirectoryName
        Path.Combine(myPath, file)

    let execute fileName arguments =
        let psi = ProcessStartInfo(fileName, arguments)
        psi.UseShellExecute <- false
        psi.CreateNoWindow <- true

        let proc = Process.Start(psi)

        proc.WaitForExit()

    let executeRead fileName arguments =
        let psi = ProcessStartInfo(fileName, arguments)
        psi.UseShellExecute <- false
        psi.CreateNoWindow <- true
        psi.RedirectStandardOutput <- true

        let proc = Process.Start(psi)

        let line = ref null

        seq {
            line := proc.StandardOutput.ReadLine()
            while !line <> null do
                yield !line
                line := proc.StandardOutput.ReadLine()
        }

    member val TfsEngine = Unchecked.defaultof<string> with get, set

    member val DbgToolsPath = Unchecked.defaultof<string> with get, set

    member private this.PdbstrPath =
        if String.IsNullOrWhiteSpace(this.DbgToolsPath) then
            relativePath @"dbgtools\pdbstr.exe"
        else
            Path.Combine(this.DbgToolsPath, "pdbstr.exe")

    member private this.ScrtoolPath =
        if String.IsNullOrWhiteSpace(this.DbgToolsPath) then
            relativePath @"dbgtools\srctool.exe"
        else
            Path.Combine(this.DbgToolsPath, "srctool.exe")

    member private this.MakeSrcsrv pdb = 
        let readPdb pdb =
            let arguments = sprintf "-r \"%s\"" pdb
            executeRead this.ScrtoolPath arguments

        let infoFiles =
            match tfsEngine with
                | Some(engine) ->
                    readPdb pdb
                    |> Seq.toList
                    |> engine.GroupByLocalWorkspaceInfo
                | None -> failwith "TFS engine not initialized."

        let serverName (wi: IWorkspaceInfo) =
            "_" + (Regex.Replace(wi.ServerUri.ToString(), "[^\w]", "_")).ToUpper()

        let indexed = ref false

        let firstCurrencyReg = Regex("^\$")

        let srcsrv =
            seq {

                yield "SRCSRV: ini ------------------------------------------------"
                yield "VERSION=3"
                yield "INDEXVERSION=2"
                yield "VERCTRL=Team Foundation Server"
                yield (sprintf "DATETIME=%s" (DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff")))
                yield "SRCSRV: variables ------------------------------------------"
                yield "TFS_EXTRACT_CMD=tf.exe view /version:%var4% /noprompt \"$%var3%\" /server:%fnvar%(%var2%) /console >%srcsrvtrg%"
                yield "TFS_EXTRACT_TARGET=%targ%\%var2%%fnbksl%(%var3%)\%var4%\%fnfile%(%var1%)"
                yield "SRCSRVVERCTRL=tfs"
                yield "SRCSRVERRDESC=access"
                yield "SRCSRVERRVAR=var2"

                let infoFilesInTfs =
                    seq {
                        for infoFile in infoFiles do
                            match (fst infoFile) with
                                | Some(info) -> yield info,snd infoFile
                                | _ -> ()

                    }

                for info in infoFiles do
                    match fst info with
                        | None ->
                            for file in (snd info) do
                                this.Log.LogWarning("File {0} not found in TFS.", file)
                        | Some(wi) ->
                            yield (sprintf "%s=%s" (serverName wi)) (wi.ServerUri.ToString())

                yield "SRCSRVTRG=%TFS_extract_target%"
                yield "SRCSRVCMD=%TFS_extract_cmd%"
                yield "SRCSRV: source files ---------------------------------------"

                for info in infoFilesInTfs do
                    let wi = fst info

                    use tpc =
                        match tfsEngine with
                            | Some(engine) -> engine.TfsTeamProjectCollection(wi.ServerUri)
                            | None -> failwith "TFS Engine not initialized"

                    let vcs = tpc.GetVersionControlServer()
                    let wsp = vcs.GetWorkspace(wi)

                    let penddingChanges =
                        wsp.GetPendingChanges(snd info)

                    for pend in penddingChanges do
                        this.Log.LogWarning("File {0} is checked out and will not be indexed.", pend.LocalItem)

                    let isPedding (item: string) =
                        let lowerItem = item.ToLower()
                        penddingChanges
                        |> Seq.exists (fun pend -> pend.LocalItem.ToLower() = lowerItem)

                    let locals =
                        wsp.GetLocalVersions(snd info)
                        |> Seq.collect id
                        |> Seq.where (fun local -> not (isPedding local.Item))

                    for local in locals do
                        indexed := true
                        yield (sprintf "%s*%s*%s*%d" local.Item (serverName wi) (firstCurrencyReg.Replace((wsp.GetServerItemForLocalItem local.Item), "")) local.Version)

                yield "SRCSRV: end ------------------------------------------------"
            }
            |> Seq.toList

        srcsrv,!indexed

    member private this.WriteSrcsrv pdb =
        let tempFile = Path.Combine((Environment.GetEnvironmentVariable("TEMP")), (Guid.NewGuid().ToString() + ".txt"))


        match this.MakeSrcsrv pdb with
            | _,false -> false
            | srcsrv,true ->
                File.AppendAllLines(tempFile, srcsrv)

                try
                    let arguments = (sprintf "-w \"-p:%s\" -s:srcsrv \"-i:%s\"" pdb tempFile)
                    execute this.PdbstrPath arguments
                finally
                    File.Delete(tempFile)
                true

    [<RequiredAttribute>]
    member val PdbFiles = Unchecked.defaultof<string> with get, set

    override this.Execute() =
        let msbuildVersion = this.BuildEngine4.GetType().Assembly.GetName().Version

        let tfsEngineAssembly =
            match this.TfsEngine with
                | "2010" -> "TfsEngine2010"
                | "2012" -> "TfsEngine2012"
                | "2013" -> "TfsEngine2013"
                | _ ->
                    if msbuildVersion >= Version(12,0,0,0) then
                        "TfsEngine2013"
                    else
                        "TfsEngine2012"

        tfsEngine <-
            Some(System.Activator.CreateInstance(tfsEngineAssembly, "TfsEngineImpl.Engine").Unwrap() :?> ITfsEngine)

        for pdbFile in this.PdbFiles.Split([|';'|]) do
            this.Log.LogMessage("Indexing file {0}...", pdbFile)

            if this.WriteSrcsrv pdbFile then
                this.Log.LogMessage("{0} indexed.", pdbFile)
            else
                this.Log.LogWarning("{0} not indexed.", pdbFile)
        true
